import { getWebsocketURL } from '@/api/system/user'
import { isLogin } from '@/utils/auth'

let socket = null // websocket 对象
const messageHandlers = new Map() // websocket 消息处理器，当接收到消息后会根据 key 找到对应的处理器进行执行

/**
 *
 * @type {{Ping: string, Logout: string, HubBaseInfo: string, OnlineUserList: string, RunInfo: string}}
 */
export const Events = {
  Ping: 'ping', // 心跳
  Logout: 'logout', // 退出连接
  OnlineUserList: 'onlineUserList', // 在线用户列表
  RunInfo: 'monitorInfo', // 系统运行消息
  HubBaseInfo: 'hubBaseInfo', // websocket 客户端信息
  forceClientLogout: 'forceClientLogout', // 强制客戶端退出
  forceUserLogout: 'forceUserLogout' // 强制用戶退出
}

/**
 * 初始化 websocket
 */
export default () => {
  log('initWebsocket')

  // 闭包函数内变量
  let lockReconnect = false // 是否允许重新连接
  let reconnectTimer = null // 重连定时器

  /**
     * 创建 websocket 对象
     */
  const createSocket = () => {
    log('createSocket')
    if (!('WebSocket' in window)) {
      log('browser nonsupport WebSocket')
      return
    }
    // 判断是否登录
    if (!isLogin()) {
      log('user not logged in')
      resetReconnect()
      return
    }
    getWebsocketURL().then(res => {
      try {
        socket = new WebSocket(res.url)
        init()
      } catch (e) {
        log('create socket is abnormal', e)
        resetReconnect()
      }
    }).catch(e => {
      log('token is abnormal', e)
      resetReconnect()
    }).finally(f => {
      log('createSocket.finally')
      lockReconnect = false
    })
  }

  /**
     * 初始化 websocket ，包括接受消息、错误处理等
     */
  const init = () => {
    log('init')

    // 已连接
    socket.onopen = () => {
      log('socket.onopen')
      // 启动心跳检测
      heartCheck.start()
    }

    // 接收消息
    socket.onmessage = (event) => {
      log('socket.onmessage')
      if (!isJsonString(event.data)) {
        log('onmessage-event: is not json string: ', event)
        return
      }

      // 重新发起心跳，避免过度发起心跳检测包，同时接收到消息则视为与服务端连接仍然有效，及时清理 pongTimeoutObj 定时器（避免主动关闭socket连接）
      heartCheck.start()

      // 数据处理
      const message = JSON.parse(event.data)
      if (message.code !== 0) {
        log('message code error', message)
      }
      messageHandle(message)
    }

    // 接收到错误消息
    socket.onerror = () => {
      log('socket.onerror')
      reconnect()
    }

    // 关闭连接
    socket.onclose = (event) => {
      log('socket.onclose', event)
      heartCheck.clear() // 删除心跳定时器
      reconnect() // 重新连接
    }

    // 页面重载
    window.onbeforeunload = () => {
      log('window.onbeforeunload')
      sendMessage(Events.Logout, '', false)
      socket.close()
    }
  }

  /**
     * 心跳检测模块
     */
  const heartCheck = {
    heartbeatTime: 1000 * 35, // 心跳检测定时器时间（秒）
    pingTimeoutObj: null, // 心跳检测定时器对象
    pongTimeoutObj: null, // 由于接收到消息后会重新发起心跳检测，因此当前定时器是为了处理时间过长未接收到服务端返回消息时视为心跳超时，自动关闭socket
    clear: function() {
      clearTimeout(this.pingTimeoutObj)
      clearTimeout(this.pongTimeoutObj)
      return this
    },
    start: function() {
      // 先删除已经启动的定时器
      this.clear()

      // 定时心跳检测
      this.pingTimeoutObj = setTimeout(() => {
        // 发起ping包
        log('ping')
        sendMessage(Events.Ping, '', false)

        // 超时未与服务器发生心跳连接可断开连接。[若收到消息，则会重启定时器，会再次调用 heartCheck.start 方法，优先清理定时器，即不会执行下面主动关闭服务]
        this.pongTimeoutObj = setTimeout(() => {
          log('pong timeout ---> auto close socket')
          socket.close()
        }, this.heartbeatTime)
      }, this.heartbeatTime)
    }
  }

  /**
     * 重新连接 ws
     */
  const reconnect = () => {
    log('reconnect: ', lockReconnect)
    if (lockReconnect) {
      return
    }
    lockReconnect = true // 标记正在连接
    log('set lockReconnect=true')

    // 2秒后重新连接
    clearTimeout(reconnectTimer)
    log('clear reconnectTimer ')
    reconnectTimer = setTimeout(() => {
      createSocket()
    }, 2000)
  }

  /**
     * 重置重连
     */
  const resetReconnect = () => {
    if (lockReconnect) {
      lockReconnect = false
    }
    reconnect()
  }

  // 创建 socket
  createSocket()

  // 注册默认的消息处理器
  registerMessageHandler()
}

function log(...data) {
  console.log('[websocket]------>> ', ...data)
}

/**
 * 消息处理，接收到服务器消息后进行消息处理，调用已注册的消息处理器进行方法回调处理
 * @param message 消息内容 JSON 对象
 */
function messageHandle(message) {
  let handled = false
  messageHandlers.forEach((func, key) => {
    if (message.event === key) {
      handled = true
      func(message) // 调用注册的函数进行回调处理，null视为不指定this参数
    }
  })

  if (!handled) {
    console.log('[WebSocket] messageHandler not registered. message-event:' + message.event)
  }
}

/**
 * 发送消息
 * @param event string 消息事件类型（对应服务端的路由，服务端会根据event调用不同的处理器）：eg：ping、logout、
 * @param data string 消息内容
 * @param isRetry boolean 消息内容
 * @returns {string}
 */
export function sendMessage(event, data, isRetry = false) {
  if (socket === null || socket === undefined || socket.readyState !== 1) {
    log('websocket is not active', event, socket)
    if (Boolean(isRetry) === true) {
      log('the connection is abnormal, wait to try again...')
      setTimeout(() => {
        sendMessage(event, data, false) // 只重试一次
      }, 5000)
      return
    }
    return
  }
  const message = JSON.stringify({
    event: event,
    data: data
  })

  try {
    socket.send(message) // 发送消息
  } catch (e) {
    if (Boolean(isRetry) === true) {
      // 重试
      log('send message error, try again in 5 second')
      setTimeout(() => {
        sendMessage(event, data, false) // 只重试一次
      }, 5000)
    }
  }
}

/**
 * 添加 websocket 消息处理器
 * @param key String 消息处理器唯一标识符
 * @param func Function 消息处理器执行的方法
 */
export function addMessageHandler(key, func) {
  log('addMessageHandler', key)
  messageHandlers.set(key, func)
}

/**
 * 删除 websocket 消息处理器
 * @param key String 消息处理器唯一标识符
 */
export function deleteMessageHandler(key) {
  log('deleteMessageHandler', key)
  messageHandlers.delete(key)
}

/**
 * 注册常规消息处理器
 */
function registerMessageHandler() {
  // 接收到服务端ping消息
  addMessageHandler(Events.Ping, (message) => {
    log('pong')
  })

  // 接收到服务端退出的消息
  addMessageHandler(Events.Logout, (message) => {
    log('logout 服务端发起关闭请求', message)
    socket.close() // 服务端发起关闭请求
  })

  // 其他消息处理器 TODO...
}

function isJsonString(value) {
  try {
    const toObj = JSON.parse(value)
    if (toObj && typeof toObj === 'object') {
      return true
    }
  } catch {
    log('not json string', value)
  }
  return false
}
